//
//  CylinderShape.cpp
//  Ray Tracer
//
//  Created by Kaila, Ashish on 3/24/13.
//  Copyright (c) 2013 Kaila, Ashish. All rights reserved.
//

#include "UnitCylinderShape.h"
#include "../Utils/Equation.h"
#include <stdlib.h>
#include <limits>

UnitCylinderShape::UnitCylinderShape(Material* material)
: SceneObject(material)
{
    
}

bool UnitCylinderShape::intersect(Ray3D& ray,
                                  const Matrix4x4& worldToModel,
                                  const Matrix4x4& modelToWorld) const
{
    Ray3D rayInObjectSpace = getRayInObjectSpace(ray, worldToModel);
    bool intersectsWithCylinder = intersectWithCylinder(rayInObjectSpace);
    bool intersectsWithCircles = intersectWithCircles(rayInObjectSpace);
    
    // No intersection
    if ((!intersectsWithCylinder) && (!intersectsWithCircles))
    {
        return false;
    }
        
    if ((!ray.intersection.none) && (ray.intersection.t_value < rayInObjectSpace.intersection.t_value))
    {
        return false;
    }
    
    ray.intersection.none = false;
    ray.intersection.t_value = rayInObjectSpace.intersection.t_value;
    ray.intersection.point = modelToWorld * rayInObjectSpace.intersection.point;
    ray.intersection.pointInObjectSpace = rayInObjectSpace.intersection.pointInObjectSpace;
    ray.intersection.normal = transNorm(worldToModel, rayInObjectSpace.intersection.normal);
    ray.intersection.normal.normalize();
    ray.intersection.obj = this;
    return true;
}

bool UnitCylinderShape::intersectWithCircles(Ray3D& rayInObjectSpace) const
{
    // Ray in xy plane can never intersect the circles
    if (rayInObjectSpace.dir[2] == 0)
    {
        return false;
    }
    
    // Let the line be defined as p + t d
    // If c is the centre of circle then (p + td - c) . n = 0 and |(p + td - c)| <= 1
    // n is either of (0, 0, 0.5) and (0, 0, -0.5)
    // => pz + tdz - cz = 0, or t = (cz - pz) / dz
    
    Point3D center(0, 0, (Vector3D(0, 0, 0.5).dot(rayInObjectSpace.dir) < 0) ? 0.5 : -0.5);
    
    double t = (center[2] - rayInObjectSpace.origin[2]) / rayInObjectSpace.dir[2];
    
    if (t < 0)
    {
        return false;
    }
    
    Point3D intersectionPoint = rayInObjectSpace.origin + t * rayInObjectSpace.dir;
    
    if ((intersectionPoint - center).length() > 1)
    {
        return false;
    }
    
    rayInObjectSpace.intersection.none = false;
    rayInObjectSpace.intersection.point = intersectionPoint;
    rayInObjectSpace.intersection.pointInObjectSpace = intersectionPoint;
    rayInObjectSpace.intersection.normal = Vector3D(0, 0, center[2]);
    rayInObjectSpace.intersection.normal.normalize();
    rayInObjectSpace.intersection.t_value = t;
    return true;
}

bool UnitCylinderShape::intersectWithCylinder(Ray3D& rayInObjectSpace) const
{
    // Let the line be defined as p + t d
    // The cylinder is defined as x ^ 2 + y ^ 2 = 1, z = z
    // If the ray intersects with cylinder then:
    // (px + t dx) ^ 2 + (py + t dy) ^ 2 = 1 and -0.5 <= pz + t dz <= 0.5
    // => px^2 + (t dx)^2 + 2 px dx t + py^2 + (t dy)^2 + 2 py dy t - 1 = 0
    // => (dx^2 + dy^2) t^2 + 2 (px dx + py dy) t + (px^2 + py^2 - 1) = 0
    
    double px = rayInObjectSpace.origin[0];
    double px2 = px * px;
    double py = rayInObjectSpace.origin[1];
    double py2 = py * py;
    double dx = rayInObjectSpace.dir[0];
    double dx2 = dx * dx;
    double dy = rayInObjectSpace.dir[1];
    double dy2 = dy * dy;
    
    std::vector<double> tValues = Equation::Evaluate(dx2 + dy2, 2 * ((px * dx) + (py * dy)), (px2 + py2 - 1));
    
    if (tValues.size() == 0)
    {
        return false;
    }
    
    double t = std::numeric_limits<double>::max();
    for (std::vector<double>::iterator tValue = tValues.begin(); tValue != tValues.end(); tValue++)
    {
        // t must be positive
        if (*tValue < 0)
        {
            continue;
        }
        
        // Compute z and verify it lies within [-0.5, 0.5]
        double z = rayInObjectSpace.origin[2] + *tValue * rayInObjectSpace.dir[2];
        
        if ((z <= 0.5) && (z >= -0.5))
        {
            // Valid t value
            if (*tValue < t)
            {
                t = *tValue;
            }
        }
    }
    
    if (t == std::numeric_limits<double>::max())
    {
        return false;
    }
    
    rayInObjectSpace.intersection.none = false;
    rayInObjectSpace.intersection.pointInObjectSpace = Point3D (px + t * dx, py + t * dy, rayInObjectSpace.origin[2] + t * rayInObjectSpace.dir[2]);
    rayInObjectSpace.intersection.point = rayInObjectSpace.intersection.pointInObjectSpace;
    rayInObjectSpace.intersection.t_value = t;
    rayInObjectSpace.intersection.normal = rayInObjectSpace.intersection.point - Point3D(0, 0, rayInObjectSpace.intersection.point[2]);
    rayInObjectSpace.intersection.normal.normalize();
    return true;
}

// Returns true if world point is contained within the object
bool UnitCylinderShape::contains(const Point3D& worldPoint, const Matrix4x4& worldToModel) const
{
    Point3D point = worldToModel * worldPoint;
    return ((point[2] <= 0.5) && (point[2] >= -0.5)) && (pow(point[0], 2) + pow(point[1], 2) <= 1.005);
}